﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Xml.Schema;
using System.IO;
using Vortex.Debugging;
using System.Xml;
using Vortex.IO;
using Vortex.Drawing;
using Vortex.Drawing.Utils;
using System.Drawing;
using System.Windows.Forms;
using Vortex.Drawing.Particles;

namespace Vortex.Resources {

	/// <summary>
	/// Implementation of collection resource file parser
	/// </summary>
	class ResourceFileParser {
		private static readonly string NAME = "Name";

		private ResourceCollection _resourceCollection;
		private string _baseDirectory;
		private string _currectSectionDirectory = "";
		private int _errorCount;
		private static XmlSchema _validationSchema;

		/// <summary>
		/// Initializes a new instance of the <see cref="ResourceFileParser"/> class.
		/// </summary>
		/// <param name="resourceCollection">The target resource collection.</param>
		public ResourceFileParser(ResourceCollection resourceCollection) {
			_resourceCollection = resourceCollection;
		}

		/// <summary>
		/// Gets the validation schema.
		/// </summary>
		/// <returns>Validation XSD schema for Vortex resource file</returns>
		private static XmlSchema GetValidationSchema() {
			if (null == _validationSchema) {
				using (Stream stream = new ResourceFileInfo("Vortex.Resources.resources.xsd").OpenStream()) {
					_validationSchema = XmlSchema.Read(stream, delegate(object sender, ValidationEventArgs args) {
						throw new ResourceException("Resource file validation schema is broken!");
					});
				}
				Log.Trace("Vortex resource file XSD schema loaded successfully");
			}
			return _validationSchema;
		}

		private void ResourceFileValidationCallback(object sender, ValidationEventArgs args) {
			Log.Error("{0} @ Line {1}, Position {2}", args.Message, args.Exception.LineNumber, args.Exception.LinePosition);
			_errorCount++;
		}

		/// <summary>
		/// Parses the specified resource file name in base directory.
		/// </summary>
		/// <param name="baseDirectory">The base directory.</param>
		/// <param name="fileName">Name of the resource file.</param>
		public void Parse(string baseDirectory, string fileName) {
			_baseDirectory = baseDirectory;

			using (Log.TraceUp("Loading resources from description [{0}]", fileName)) {
				XmlDocument document = new XmlDocument();
				using (Stream stream = new ResourceFileInfo(fileName).OpenStream()) {
					document.Load(stream);
				}
				//validate document
				document.Schemas.Add(GetValidationSchema());
				document.Validate(new ValidationEventHandler(ResourceFileValidationCallback));
				if (_errorCount > 0) {
					throw new ResourceFileParseException("Resource file validation has been failed. {0} error(s) found", _errorCount);
				}

				ProcessSprites(document);
				ProcessSpriteCollections(document);
				ProcessFonts(document);
				ProcessShaders(document);
				ProcessParticleEffects(document);
				ProcessCursors(document);

				/*
				XmlNodeList atlases = document.GetElementsByTagName("Atlas");
				XmlNodeList textures = document.GetElementsByTagName("Texture");
				XmlNodeList lists = document.GetElementsByTagName("List");
				XmlNodeList grids = document.GetElementsByTagName("Grid");
				XmlNodeList fonts = document.GetElementsByTagName("Font");
				XmlNodeList cursors = document.GetElementsByTagName("Cursor");

				ProcessResourceSection(document, ResourceSection.Sprites);
				ProcessResourceSection(document, ResourceSection.Fonts);
				ProcessResourceSection(document, ResourceSection.Cursors);

				ParseAtlases(atlases);
				ParseTextures(textures);
				ParseLists(lists);
				ParseGrids(grids);
				ParseFonts(fonts);
				ParseCursors(cursors);
				*/
			}
		}

		private void ProcessSprites(XmlDocument document) {
			foreach (XmlNode node in document.GetElementsByTagName("Sprites")) {
				//enter section first
				EnterResourceSection(node);

				//then try iterate all of atlases
				foreach (XmlNode atlasNode in node.SelectNodes("AtlasTexture")) {
					ParseAtlasNode(atlasNode);
				}

				//at last single textures should be processed
				foreach (XmlNode textureNode in node.SelectNodes("Texture")) {
					ParseTextureNode(textureNode);
				}
			}
		}

		/// <summary>
		/// Processes the sprite collections section.
		/// </summary>
		/// <param name="document">The document.</param>
		private void ProcessSpriteCollections(XmlDocument document) {
			foreach (XmlNode node in document.GetElementsByTagName("SpriteCollections")) {
				//enter section first
				EnterResourceSection(node);

				//then try iterate all of atlases
				foreach (XmlNode gridNode in node.SelectNodes("Grid")) {
					ParseGridNode(gridNode);
				}

				//at last single textures should be processed
				foreach (XmlNode listNode in node.SelectNodes("List")) {
					ParseListNode(listNode);
				}
			}
		}

		/// <summary>
		/// Processes the fonts section.
		/// </summary>
		/// <param name="document">The document.</param>
		private void ProcessFonts(XmlDocument document) {
			foreach (XmlNode node in document.GetElementsByTagName("Fonts")) {
				//enter section first
				EnterResourceSection(node);

				//then try iterate all of atlases
				foreach (XmlNode fontNode in node.SelectNodes("Font")) {
					ParseFontNode(fontNode);
				}
			}
		}

		private void ProcessShaders(XmlDocument document) {
			foreach (XmlNode node in document.GetElementsByTagName("Shaders")) {
				//enter section first
				EnterResourceSection(node);

				//then try iterate all of atlases
				foreach (XmlNode pixelShaderNode in node.SelectNodes("PixelShader")) {
					ParsePixelShaderNode(pixelShaderNode);
				}

				//at last single textures should be processed
				foreach (XmlNode vertexShaderNode in node.SelectNodes("VortexShader")) {
					ParseVortexShaderNode(vertexShaderNode);
				}
			}			
		}

		private void ProcessParticleEffects(XmlDocument document) {
			foreach (XmlNode node in document.GetElementsByTagName("ParticleEffects")) {
				//enter section first
				EnterResourceSection(node);

				//then try iterate all of atlases
				foreach (XmlNode effectNode in node.SelectNodes("Effect")) {
					ParseParticleEffectNode(effectNode);
				}
			}				
		}

		/// <summary>
		/// Processes the cursors section.
		/// </summary>
		/// <param name="document">The xml document.</param>
		private void ProcessCursors(XmlDocument document) {
			foreach (XmlNode node in document.GetElementsByTagName("Cursors")) {
				//enter section first
				EnterResourceSection(node);

				//then try iterate all of atlases
				foreach (XmlNode fontNode in node.SelectNodes("Cursor")) {
					ParseCursorNode(fontNode);
				}
			}			
		}

		/// <summary>
		/// Enters the resource section.
		/// </summary>
		/// <param name="node">The node defining section.</param>
		private void EnterResourceSection(XmlNode node) {
			string baseDir = GetAttrValue(node, "BaseDir");
			if (!string.IsNullOrEmpty(baseDir)) {
				if (PathUtils.IsPathRooted(baseDir)) {
					_currectSectionDirectory = baseDir;
				} else {
					_currectSectionDirectory = _baseDirectory + @"\" + baseDir;
				}
			}
		}

		/// <summary>
		/// Makes the resource path based on file name and resource class.
		/// </summary>
		/// <param name="directoryBase">Defines directory base.</param>
		/// <param name="fileName">Name of the file.</param>
		/// <returns>
		/// Application dir relative path for specified fileName and resource class
		/// </returns>
		private string MakeResourcePath(string fileName) {
			//handling application base path case
			if (PathUtils.IsPathContextRooted(fileName)) {
				return PathUtils.EscapeContextRootedPath(fileName);
			} else if (PathUtils.IsPathRooted(fileName)) {
				return fileName;
			} else {
				return _currectSectionDirectory + @"\" + fileName;
			}
		}

		/// <summary>
		/// Parses atlas node.
		/// </summary>
		/// <param name="atlasNode">The atlas node.</param>
		private void ParseAtlasNode(XmlNode atlasNode) {
			XmlNodeList imageList = atlasNode.SelectNodes("Image");

			string atlasId = GetAttrValue(atlasNode, NAME);				
			string filter = GetAttrValue(atlasNode, "Filter");
			PixelFormat pixelFormat = (PixelFormat)Enum.Parse(typeof(PixelFormat), GetAttrValue(atlasNode, "Format", PixelFormat.A8R8G8B8.ToString()), true);
			bool useBorder = "true".Equals(GetAttrValue(atlasNode, "Border", "false"), StringComparison.InvariantCultureIgnoreCase);

			//create/init texture...
			BitmapAtlas atlas = new BitmapAtlas();

			//process images
			ProcessImages(atlas, imageList);
			
			Texture texture = atlas.ToTexture(pixelFormat, useBorder ? new Size(1, 1) : Size.Empty);
			_resourceCollection.AddDisposable(texture);

			if (null != filter) {
				texture.Options.Filter = (TextureFilter)Enum.Parse(typeof(TextureFilter), filter, true);
			}

			Sprite textureSprite = texture.ToSprite();
			//add texture as sprite if it is requested
			if (null != atlasId) {
				_resourceCollection.Add(atlasId, textureSprite);
			}

			ProcessImageRegions(texture, imageList);
		}

		/// <summary>
		/// Processes image regions after atlas is assembled.
		/// </summary>
		/// <param name="texture">The texture of atlas.</param>
		/// <param name="imageList">The image node list.</param>
		private void ProcessImageRegions(Texture texture, XmlNodeList imageList) {
			foreach (XmlNode imageNode in imageList) {
				string imageId = GetAttrValue(imageNode, NAME);
				Sprite imageSprite = texture.Sprites[GetImageNameForAtlas(imageNode)];
				if (imageId != null) {
					_resourceCollection.Add(imageId, CheckChangeOrigin(imageSprite, imageNode));
				}

				ProcessRegions(imageSprite, imageNode.SelectNodes("Region"));
			}
		}

		/// <summary>
		/// Processes image node list and populates atlas with images.
		/// </summary>
		/// <param name="atlas">The atlas.</param>
		/// <param name="xmlNodeList">The XML node list.</param>
		private void ProcessImages(BitmapAtlas atlas, XmlNodeList xmlNodeList) {
			foreach (XmlNode imageNode in xmlNodeList) {
				string imageId = GetImageNameForAtlas(imageNode);
				string fileName = GetFileNameAttribute(imageNode, "File");
				atlas.AddImage(imageId, fileName);
			}
		}

		/// <summary>
		/// Gets the temporary id of image for atlas.
		/// </summary>
		/// <param name="imageNode">The image node.</param>
		/// <returns>Temporary id of image: Id or File</returns>
		private string GetImageNameForAtlas(XmlNode imageNode) {
			return GetAttrValue(imageNode, NAME) ?? GetAttrValue(imageNode, "File");
		}

		/// <summary>
		/// Parses the enum string value.
		/// </summary>
		/// <typeparam name="T">Type of enum</typeparam>
		/// <param name="value">The value.</param>
		/// <returns>Enum value</returns>
		private static T ParseEnum<T>(string value) {
			return (T)Enum.Parse(typeof(T), value, true);
		}

		/// <summary>
		/// Parses texture node.
		/// </summary>
		/// <param name="textures">The texture node.</param>
		private void ParseTextureNode(XmlNode textureNode) {
			string format = GetAttrValue(textureNode, "Format");
			string filter = GetAttrValue(textureNode, "Filter");

			XmlNode imageNode = textureNode.SelectSingleNode("Image");
			string imageFile = GetFileNameAttribute(imageNode, "File");
			string imageName = GetAttrValue(imageNode, NAME);
			

			Texture texture;
			if (format != null) {
				texture = new Texture(imageFile, ParseEnum<PixelFormat>(format));				
			} else {
				texture = new Texture(imageFile);
			}

			_resourceCollection.AddDisposable(texture);

			if (null != filter) {
				texture.Options.Filter = ParseEnum<TextureFilter>(filter);
			}

			Sprite textureSprite = texture.ToSprite();
			//add texture as sprite if it is requested
			if (null != imageName) {
				_resourceCollection.Add(imageName, CheckChangeOrigin(textureSprite, imageNode));
			}

			ProcessRegions(textureSprite, imageNode.SelectNodes("Region"));
		}

		/// <summary>
		/// Processes the regions inside specified sprite.
		/// </summary>
		/// <param name="textureSprite">The parent sprite.</param>
		/// <param name="xmlNodeList">The XML node list with region definition.</param>
		private void ProcessRegions(Sprite containerSprite, XmlNodeList xmlNodeList) {
			foreach (XmlNode regionNode in xmlNodeList) {
				string regionName = GetAttrValue(regionNode, NAME);
				int x = Int32.Parse(GetAttrValue(regionNode, "X"));
				int y = Int32.Parse(GetAttrValue(regionNode, "Y"));
				int width = Int32.Parse(GetAttrValue(regionNode, "Width"));
				int height = Int32.Parse(GetAttrValue(regionNode, "Height"));

				Rect region = new Rect(x, y, x + width, y + height);
				_resourceCollection.Add(regionName, CheckChangeOrigin(containerSprite.Cut(region), regionNode));
			}
		}

		/// <summary>
		/// Checks the change of sprite origin in the XML.
		/// </summary>
		/// <param name="sprite">The sprite origin changes.</param>
		/// <param name="node">The node to check.</param>
		/// <returns><see cref="System.Drawing.Sprite"/> with changed origin</returns>
		private Sprite CheckChangeOrigin(Sprite sprite, XmlNode node) {
			Vector2 origin = sprite.Origin;
			string oxString = GetAttrValue(node, "OriginX");
			string oyString = GetAttrValue(node, "OriginY");
			if (null != oxString || null != oxString) {
				if (null != oxString) origin.X = (float)int.Parse(oxString);
				if (null != oxString) origin.Y = (float)int.Parse(oyString);
				return sprite.MoveOriginTo(origin);
			} else {
				return sprite;
			}
		}

		/// <summary>
		/// Processes the sprite lists in resources.
		/// </summary>
		/// <param name="listNode">The list node.</param>
		private void ParseListNode(XmlNode listNode) {
			string listId = GetAttrValue(listNode, NAME);
			SpriteList list = new SpriteList();
			foreach (XmlNode itemNode in listNode.SelectNodes("Item")) {
				ProcessListItems(list, itemNode);
			}
			//add list into collection
			_resourceCollection.Add(listId, list);
		}

		/// <summary>
		/// Processes the sprite list items.
		/// </summary>
		/// <param name="list">The container list.</param>
		/// <param name="itemNode">The item node.</param>
		private void ProcessListItems(SpriteList list, XmlNode itemNode) {
			string refId = GetAttrValue(itemNode, "Ref");
			Sprite spriteRef = _resourceCollection.Get<Sprite>(refId);
			list.Add(spriteRef);

			//add all of sprites *_id from collection
		}

		/// <summary>
		/// Processes the sprite grid items.
		/// </summary>
		/// <param name="itemNode">The item node.</param>
		private void ParseGridNode(XmlNode gridNode) {
			string id = GetAttrValue(gridNode, NAME);
			string refId = GetAttrValue(gridNode, "SourceRef");
			int splitX = int.Parse(GetAttrValue(gridNode, "Width"));
			int splitY = int.Parse(GetAttrValue(gridNode, "Height"));
			//try to resolve sprite
			Sprite spriteRef = _resourceCollection.Get<Sprite>(refId);

			SpriteGrid grid = new SpriteGrid(spriteRef, splitX, splitY);
			_resourceCollection.Add(id, grid);

			//add all of sprites *_id from collection
		}

		#region Fonts

		/// <summary>
		/// Parses the fonts.
		/// </summary>
		/// <param name="fontNode">The font node.</param>
		private void ParseFontNode(XmlNode fontNode) {
			string name = GetAttrValue(fontNode, NAME);
			string glyphs = GetAttrValue(fontNode, "Glyphs");

			SpriteFont font;
			if (null != glyphs) {
				Sprite glyphsSprite = _resourceCollection.Get<Sprite>(glyphs);
				font = new SpriteFont(MakeResourcePath(name + ".fnt"), glyphsSprite); 
			} else {
				font = new SpriteFont(MakeResourcePath(name));
			}
			_resourceCollection.AddDisposable(font);
			_resourceCollection.Add(name, font);
		}

		#endregion

		#region Cursors

		/// <summary>
		/// Parses cursor node.
		/// </summary>
		/// <param name="cursorTag">The cursor tag.</param>
		private void ParseCursorNode(XmlNode cursorTag) {
			string id = GetAttrValue(cursorTag, NAME);
			string systemName = GetAttrValue(cursorTag, "SystemName");
			string cursorFile = GetFileNameAttribute(cursorTag, "File");				
			string spriteRef = GetAttrValue(cursorTag, "SpriteRef");

			if (null != systemName && null != cursorFile) {
				throw new ResourceFileParseException("SystemName and Cursor file both appears in cursor definition. But they are mutual exclusinve.");
			}

			Cursor systemCursor = Cursors.Default;
			Sprite cursorSprite = null;
			//try to get cursor from system cursor name
			if (null != systemName) {
				systemCursor = ResolveCursorSystemName(systemName);
			}
			//attempt to load cursor from file or from system resources
			if (null != cursorFile) {
				systemCursor = LoadCursor(cursorFile);
				_resourceCollection.AddDisposable(systemCursor);
			}
			if (null != spriteRef) {
				cursorSprite = _resourceCollection.Get<Sprite>(spriteRef);
			}

			//create cursor and add into resources
			GameCursor cursor =
				cursorSprite != null ?
				new GameCursor(systemCursor, cursorSprite) :
				new GameCursor(systemCursor);

			_resourceCollection.Add(id, cursor);
		}

		/// <summary>
		/// Resolves the cursor by its system name.
		/// </summary>
		/// <param name="systemName">Name of the system cursor.</param>
		/// <returns>System cursor</returns>
		private Cursor ResolveCursorSystemName(string systemName) {
			System.Reflection.PropertyInfo info = typeof(Cursors).GetProperty(systemName, System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public);
			if (null == info) {
				throw new ResourceFileParseException("System cursor name '{0}' can't be resolved!", systemName);
			}
			return (Cursor)info.GetValue(null, null);
		}

		/// <summary>
		/// Loads the cursor.
		/// </summary>
		/// <param name="cursorFile">The cursor file.</param>
		/// <returns>System cursor</returns>
		private Cursor LoadCursor(string cursorFile) {
			ResourceFileInfo fileInfo = new ResourceFileInfo(cursorFile);
			using (Stream stream = fileInfo.OpenStream()) {
				//TODO: fix cursor loading. For some reasons cursor format is bad...
				return new Cursor(stream);
			}
		}

		#endregion

		#region Parsing Particle Effects


		/// <summary>
		/// Parses the particle effect node.
		/// </summary>
		/// <param name="effectNode">The effect node.</param>
		private void ParseParticleEffectNode(XmlNode node) {
			string name = GetAttrValue(node, NAME);
			string fileName = GetFileNameAttribute(node, "File");
			string spriteRef = GetAttrValue(node, "SpriteRef");

			//get sprite collection
			ISpriteSource spriteSource = _resourceCollection.Get<ISpriteSource>(spriteRef);

			//create effect from file description and sprite source
			ParticleEffect effect = new ParticleEffect(fileName, spriteSource);
			//add effect to collection
			_resourceCollection.Add(name, effect);
		}

		#endregion

		#region Parsing Shaders

		/// <summary>
		/// Parses the pixel shader node.
		/// </summary>
		/// <param name="node">The node.</param>
		private void ParsePixelShaderNode(XmlNode node) {
			string name = GetAttrValue(node, NAME);
			string fileName = GetFileNameAttribute(node, "File");
			string entryPoint = GetAttrValue(node, "EntryPoint") ?? PixelShader.DefaultEntryPoint;
			string profile = GetAttrValue(node, "Profile");			
			PixelShaderProfile psProfile = profile != null ? ParsePSProfile(profile) : PixelShader.DefaultProfile;

			//create pixel shader
			PixelShader shader = new PixelShader(fileName, psProfile, entryPoint);
			//add pixel shader into collection
			_resourceCollection.Add(name, shader);
		}

		/// <summary>
		/// Parses the PS profile string.
		/// </summary>
		/// <param name="profile">The profile string.</param>
		/// <returns>Pixel shader profile</returns>
		private PixelShaderProfile ParsePSProfile(string profile) {
			switch (profile) {
				case "1.0": return PixelShaderProfile.Ps_1_0;
				case "1.4": return PixelShaderProfile.Ps_1_4;
				case "2.0": return PixelShaderProfile.Ps_2_0;
				case "3.0": return PixelShaderProfile.Ps_3_0;
				default: throw new ResourceFileParseException("Unknown pixel shader profile '{0}'", profile);
			}
		}

		/// <summary>
		/// Parses the vortex shader node.
		/// </summary>
		/// <param name="node">The node.</param>
		private void ParseVortexShaderNode(XmlNode node) {
			string name = GetAttrValue(node, NAME);
			string fileName = GetFileNameAttribute(node, "File");
			string entryPoint = GetAttrValue(node, "EntryPoint") ?? VertexShader.DefaultEntryPoint;
			string profile = GetAttrValue(node, "Profile");			
			VertexShaderProfile vsProfile = profile != null ? ParseVSProfile(profile) : VertexShader.DefaultProfile;

			//create pixel shader
			VertexShader shader = new VertexShader(fileName, vsProfile, entryPoint);
			//add pixel shader into collection
			_resourceCollection.Add(name, shader);
		}

		/// <summary>
		/// Parses the VS profile string.
		/// </summary>
		/// <param name="profile">The profile string.</param>
		/// <returns>Pixel shader profile</returns>
		private VertexShaderProfile ParseVSProfile(string profile) {
			switch (profile) {
				case "1.1": return VertexShaderProfile.Vs_1_1;
				case "2.0": return VertexShaderProfile.Vs_2_0;
				case "3.0": return VertexShaderProfile.Vs_3_0;
				default: throw new ResourceFileParseException("Unknown vertex shader profile '{0}'", profile);
			}
		}

		#endregion

		#region Helper Methods


		/// <summary>
		/// Gets the filename attribute.
		/// </summary>
		/// <param name="node">The node.</param>
		/// <param name="fileAtrributeName">Name of the file attribute.</param>
		/// <returns></returns>
		private string GetFileNameAttribute(XmlNode node, string fileAttributeName) {
			string fileName = GetAttrValue(node, fileAttributeName);
			return fileName != null ? MakeResourcePath(fileName) : null;
		}

		/// <summary>
		/// Gets the xml attribute value.
		/// </summary>
		/// <param name="textureNode">The node.</param>
		/// <param name="attributeName">Name of the attribute.</param>
		/// <returns>Value of attribute if it exists in node or <c>null</c></returns>
		private string GetAttrValue(XmlNode node, string attributeName) {
			XmlAttribute attribute = node.Attributes[attributeName];
			return attribute != null ? attribute.Value : null;
		}

		/// <summary>
		/// Gets the xml attribute value.
		/// </summary>
		/// <param name="node">The node.</param>
		/// <param name="attributeName">Name of the attribute.</param>
		/// <param name="defaultValue">The default value.</param>
		/// <returns>
		/// Value of attribute if it exists in node or default value
		/// </returns>
		private string GetAttrValue(XmlNode node, string attributeName, string defaultValue) {
			XmlAttribute attribute = node.Attributes[attributeName];
			return attribute != null ? attribute.Value : defaultValue;
		}

		#endregion
	}
}
